package svc.elib.util;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.util.Properties;

/**
To tackle the problem of inefficient file I/O, we need a buffered RandomAccessFile class. 
A new class is derived from the RandomAccessFile class, in order to reuse all the methods in it. 
**/

public final class FastRAFReader extends RandomAccessFile {
		
/**
For efficiency reasons, we define a byte _fileBuffer instead of char _fileBuffer. 
The variables buf_end, buf_pos, and real_pos are used to record the effective positions on the _fileBuffer:
**/
  private static final StringBuilder EMPTY_SB = new StringBuilder("");
  //Backing arrays
  private byte _fileBuffer[];
  private byte _lineBuffer[];
  
  private ByteBuffer lineBuffer; 
  private ByteBuffer fileBuffer;
  
  private CharBuffer charBuff; 
  
  private int buf_end = 0;
  private int buf_pos = 0;
  private long real_pos = 0;
  private int BUFFER_SIZE = 32768;
  private int MAX_LINE_LENGTH = 16384;
  private char FIELD_DELIMITER = '|';
  private int MAX_FIELDS = 64;
  private String FILE_ENCODING = "UTF-8";
  private StringBuilder flds[];
  private int parsedFields;
  
  private StringBuilder lineSB; 
  
  private CharsetDecoder cd;
  
  private int lineNoo;
  private long oldLineOffset=0, newLineOffset=0;
  
  
/**
A new constructor is added 
**/

  private static String getMode(Properties prop) {
	    
		if("true".equalsIgnoreCase(prop.getProperty( "Writable" , "false")))  
		    return "rws";
		else
			return "r";
  }
  
  public FastRAFReader(Properties prop) throws IOException {
	super(prop.getProperty( "FileToRead" ),getMode(prop));  
	invalidate();
    
    BUFFER_SIZE = Integer.parseInt( prop.getProperty( "BufferSize", "32768" ) );
    MAX_LINE_LENGTH = Integer.parseInt( prop.getProperty( "MaxLineLength", "16384" ) );
    MAX_FIELDS = Integer.parseInt( prop.getProperty( "MaxFields", "64" ) );
    FILE_ENCODING = prop.getProperty( "TextEncoding" , "US-ASCII");
    FIELD_DELIMITER = prop.getProperty( "FieldSeparator", "|" ).charAt(0);  
   
    _fileBuffer = new byte[BUFFER_SIZE];
    _lineBuffer = new byte[MAX_LINE_LENGTH];
    lineBuffer = ByteBuffer.wrap(_lineBuffer);
    fileBuffer = ByteBuffer.wrap(_fileBuffer);
    
    lineNoo = 0;
    
    flds = new StringBuilder[MAX_FIELDS];
    for(int i = 0; i<flds.length;i++){
    flds[i] = new StringBuilder(0);
    }
    parsedFields = 0;
    
    
    //if("".equals(FILE_ENCODING)) FILE_ENCODING = "US-ASCII"; // TODO: is this Ok?

    
    Charset CSInstance;
      
    if("".equals(FILE_ENCODING))
         CSInstance = Charset.defaultCharset();
    else
         CSInstance = Charset.forName(FILE_ENCODING);
      
    cd = CSInstance.newDecoder()
       	    .onMalformedInput(CodingErrorAction.REPLACE)
    	    .onUnmappableCharacter(CodingErrorAction.REPORT); 
      
    
    charBuff = CharBuffer.allocate(MAX_LINE_LENGTH);

    lineSB = new StringBuilder(1024);
  }
  
  

/**
The new read method is written such that it always reads from the _fileBuffer first. 
It overrides the native read method in the original class, which is never engaged until the _fileBuffer 
has run out of room. In that case, the fillBuffer method is called to fill in the _fileBuffer. In fillBuffer, 
the original read is invoked. The private method invalidate is used to indicate that the _fileBuffer no 
longer contains valid contents. This is necessary when the seek method moves the file pointer 
out of the _fileBuffer.
**/


  public final int read() throws IOException{
    if(buf_pos >= buf_end) 
    {
       if(fillBuffer() < 0)
       return -1;
    }
    if(buf_end == 0) 
        return -1;

    return _fileBuffer[buf_pos++];
    
  }
  
  private int fillBuffer() throws IOException {
    int n = super.read(_fileBuffer, 0, BUFFER_SIZE);
    if(n >= 0) {
      real_pos +=n;
      buf_end = n;
      buf_pos = 0;
    }
    return n;
  }
  
  public void invalidate() throws IOException {
    buf_end = 0;
    buf_pos = 0;
    real_pos = super.getFilePointer();
  }


/**
The other parameterized read method also is overridden. The code for the new read is listed below. 
If there is enough _fileBuffer, it will simply call System.arraycopy to copy a portion of the _fileBuffer directly 
into the user-provided area. This presents the most significant performance gain because the read method is 
heavily used in the getNextLine method, which is our replacement for readLine. 
**/
  public int read(byte b[], int off, int len) throws IOException {
   lineNoo = 0;
   int leftover = buf_end - buf_pos;
   
   if(len <= leftover) {
             System.arraycopy(_fileBuffer, buf_pos, b, off, len);
        buf_pos += len;
        return len;
   }
   for(int i = 0; i < len; i++) {
      int c = this.read();
      if(c != -1)
         b[off+i] = (byte)c;
      else {
         return i;
      }
   }
   return len;
  }


/**
The original methods getFilePointer and seek need to be overridden as well in order to take advantage of the _fileBuffer.
Most of time, both methods will simply operate inside the _fileBuffer.
**/
  public long getFilePointer(){
    long l = real_pos;
    return (l - buf_end + buf_pos) ;
  }
  
  public void seek(long pos) throws IOException {
  	lineNoo = 0; // reset counter after this
    int n = (int)(real_pos - pos);
    if(n >= 0 && n <= buf_end) {
      buf_pos = buf_end - n;
    } else {
      super.seek(pos);
      invalidate();
    }
    newLineOffset = this.getFilePointer();
  }


/**
Most important, a new method, nextLine, is added to replace the readLine method. 
We can not simply override the readLine method because it is defined as final in the original class. 
The getNextLine method first decides if the _fileBuffer still contains unread contents. 
If it doesn't, the _fileBuffer needs to be filled up. 
If the new line delimiter can be found in the _fileBuffer, then a new line is read from the _fileBuffer and 
converted into StringBuilder. Otherwise, it will simply call the read method to read byte by byte. 
Although the code of the latter portion is similar to the original readLine, performance is better 
here because the read method is buffered in the new class.
**/
  /**
   * return -1 for end of file 
   */  
  public final int nextLine() throws IOException {
   lineSB.setLength(0);
   lineNoo++;
   if(buf_end-buf_pos <= 0) {
      if(fillBuffer() < 0) {
      	 newLineOffset = this.getFilePointer();
         return -1;
      }
   }
   
   int lineend = nextLineEndPos();
   charBuff.clear();
   oldLineOffset = this.getFilePointer();
   if(lineend < 0) {	
     	int k = this.read(_lineBuffer, 0, buf_end - buf_pos); 		   	    
    	this.fillBuffer(); 

        int c = read();
        
        while ((c != '\n') && (c  != -1)) 
        {
             if(c == '\r') 
                 {  c = read(); 
                    continue; 
                 }
          
               if(k<MAX_LINE_LENGTH) { // Important: chars too many are silently IGNORED!
                  _lineBuffer[k] = (byte)c;
                  k++;
               }
               c = read();
        }
        if ((c == -1) && (k == 0)) {
          return -1;
        } else if( k == 0){ 
        	separateFields();
        	newLineOffset = this.getFilePointer();
        	return lineNoo; //Empty line
        }
        
        lineBuffer.limit(k);
        lineBuffer.position(0);
        
        cd.decode(lineBuffer, charBuff, true);
        
        charBuff.flip();
        lineSB.append(charBuff);
        
        //System.out.println("Line: |" + lineSB + "|");
        int le = lineSB.length();
        char xx = lineSB.charAt(le-1);
        while(le>0 && (xx =='\r' || xx == '\n')) 
            { lineSB.deleteCharAt(le-1);
              le--;
              
              if (le > 0)
            	  xx = lineSB.charAt(le-1);
            }
        
        separateFields(); 
        newLineOffset = this.getFilePointer();
        return lineNoo;
   }
   
   
   if(lineend > 0 && _fileBuffer[lineend-1] == '\r'){
   		fileBuffer.limit(lineend - 1);
   } else {
   		fileBuffer.limit(lineend);
   }
   
   fileBuffer.position(buf_pos);
   buf_pos = lineend + 1;
   
   cd.decode(fileBuffer, charBuff, true);
      
   charBuff.flip();
   lineSB.append(charBuff);  
   
   separateFields(); 
   newLineOffset = this.getFilePointer();
   return lineNoo;
   }
  
  
  private int nextLineEndPos(){
    int lineend = -1;
    for(int i = buf_pos; i < buf_end; i++) {
         if(_fileBuffer[i] == '\n') {
          lineend = i;
           break;
           }
    }
    return lineend;
  }
  
  
  /**
   * @return the last read line offset
   */
  public long getCurrentLineOffset()
  { return oldLineOffset;
  }
  
  /**
   * @return the next line to be read offset
   */
  public long getNextLineOffset()
  { return newLineOffset;
  }

  /** This method tokenizes line into fields, watch out, it supports only one delimiter (speed!)
  **/
  private void separateFields(){
  	int i = 0, old_i = 0;
 
  	parsedFields = 0;
  	while(-1 != (i = StringUtil.indexOf(lineSB, FIELD_DELIMITER, i))){
  	//while(-1 != (i = lineSB.indexOf(FIELD_DELIMITER, i))){
  		//System.out.println(lineSB.subSequence(old_i,i)); 		
  		flds[parsedFields].setLength(0); 
  		flds[parsedFields].append(lineSB,old_i,i);
  		
  		parsedFields++;
  		i++;
  		old_i = i;
  	}
  	
  	int len = lineSB.length();
  	if(old_i >= len){ 
  		//System.out.println("ONE EMPTY MORE");
  		flds[parsedFields].setLength(0);
  		parsedFields++;
  	}else { 
  		if(parsedFields<MAX_FIELDS) { // Important: fields too many are silently IGNORED!
  			flds[parsedFields].setLength(0);
  			flds[parsedFields].append(lineSB, old_i, len); 
  			parsedFields++;
  		}

  	}
  	
    
  }
  
  public StringBuilder getLastLineAsSBU(){
  	return lineSB;
  }

  //parsedFields contains number of last parsed fields
  public StringBuilder getFieldAsSBR(int n){       	 	
  	if((n < 0) || (n > parsedFields - 1)) return EMPTY_SB;
  	return flds[n]; 
  }
  
  public int getNooFields()
  {   return parsedFields;
  }
  
  public void close() throws IOException{
  	_fileBuffer = null;
    _lineBuffer = null;
    lineBuffer = null;
    fileBuffer = null;
    super.close();
  }

  
}